import Foundation


public extension Encodable {

    func toYson() -> YsonValue {
        let e = JsonEncoder()
        try? self.encode(to: e)
        return e.jsonValue
    }

    func toYson(_ options: JsonEncoderOptions) -> YsonValue {
        let e = JsonEncoder(options: options)
        try? self.encode(to: e)
        return e.jsonValue
    }

    func toYson(_ block: (JsonEncoderOptions) -> Void) -> YsonValue {
        let e = JsonEncoder()
        block(e.options)
        try? self.encode(to: e)
        return e.jsonValue
    }
}

public protocol JsonEncoderAction {
    func match(_ type: Any.Type) -> Bool
    func encodeValue(_ type: Any.Type, _ value: Any) -> YsonValue
}


public struct DateSecondsJsonEncoder: JsonEncoderAction {
    public init() {
    }

    public func match(_ type: Any.Type) -> Bool {
        type == Date.self || type == NSDate.self
    }

    public func encodeValue(_ type: Any.Type, _ value: Any) -> YsonValue {
        if let d = value as? Date {
            return YsonNum(d.timeIntervalSince1970.intValue)
        }
        if let d = value as? NSDate {
            return YsonNum(d.timeIntervalSince1970.intValue)
        }
        return YsonNull.inst
    }
}

public struct DateMillSecondsJsonEncoder: JsonEncoderAction {

    public init() {
    }

    public func match(_ type: Any.Type) -> Bool {
        type == Date.self || type == NSDate.self
    }

    public func encodeValue(_ type: Any.Type, _ value: Any) -> YsonValue {
        if let d = value as? Date {
            return YsonNum((d.timeIntervalSince1970 * 1000).longValue)
        }
        if let d = value as? NSDate {
            return YsonNum((d.timeIntervalSince1970 * 1000).longValue)
        }
        return YsonNull.inst
    }
}

public struct DateFormatJsonEncoder: JsonEncoderAction {
    let dateFormat: String

    public init(_ fmt: String) {
        dateFormat = fmt
    }

    public static let timestampX: DateFormatJsonEncoder = DateFormatJsonEncoder("yyyy-MM-dd HH:mm:ss.SSS")
    public static let timestamp: DateFormatJsonEncoder = DateFormatJsonEncoder("yyyy-MM-dd HH:mm:ss")
    public static let date: DateFormatJsonEncoder = DateFormatJsonEncoder("yyyy-MM-dd")
    public static let time: DateFormatJsonEncoder = DateFormatJsonEncoder("HH:mm:ss")
    public static let UTC: DateFormatJsonEncoder = DateFormatJsonEncoder("yyyy-MM-dd'T'HH:mm:ssZ")
    public static let GMT: DateFormatJsonEncoder = DateFormatJsonEncoder("ccc, dd MMM yyyy HH:mm:ss 'GMT'")

    public func match(_ type: Any.Type) -> Bool {
        type == Date.self || type == NSDate.self
    }

    public func encodeValue(_ type: Any.Type, _ value: Any) -> YsonValue {
        if let d = value as? Date {
            return YsonString(d.format(fmt: dateFormat))
        }
        return YsonNull.inst
    }
}

public class JsonEncoderOptions: Applyable {

    var allowNull: Bool = false
    fileprivate var encoders: [JsonEncoderAction] = []
    //fieldName -> JsonName
    fileprivate var namesMap: [String: String] = [:]

    public init() {

    }

    @discardableResult
    public func nameMap(field: String, json: String) -> JsonEncoderOptions {
        namesMap[field] = json
        return self
    }

    @discardableResult
    public func push(_ d: JsonEncoderAction) -> JsonEncoderOptions {
        encoders.insert(d, at: 0)
        return self
    }

    public func dateFormat(_ fmt: String) {
        push(DateFormatJsonEncoder(fmt))
    }

    public func dateSeconds1970() {
        push(DateSecondsJsonEncoder())
    }

    public func dateMillSeconds1970() {
        push(DateMillSecondsJsonEncoder())
    }

    fileprivate func encode(_ type: Any.Type, _ value: Any) -> (Bool, YsonValue) {
        for d in encoders {
            if (d.match(type)) {
                return (true, d.encodeValue(type, value))
            }
        }
        return (false, YsonNull.inst)
    }
}

public class JsonEncoder: Encoder, Applyable {

    public var codingPath: [CodingKey] = []
    public var userInfo: [CodingUserInfoKey: Any] = [:]
    fileprivate let options: JsonEncoderOptions
    private var valueList: [YsonValue] = []
    fileprivate var parent: JsonEncoder? = nil
    fileprivate var parentKey: String? = nil

    public init(options: JsonEncoderOptions = JsonEncoderOptions()) {
        self.options = options
    }

    public var jsonValue: YsonValue {
//        for yv in valueList {
//            println("---", yv.yson)
//        }
        if valueList.count < 2 {
            return valueList.first ?? YsonNull.inst
        }
        let first = valueList.first!

        switch first {
        case let ob as YsonObject:
            for i in 1..<valueList.count {
                if let vo = valueList[i] as? YsonObject {
                    for (k, v) in vo {
                        ob.put(k, v)
                    }
                }
            }
            return ob
        case let ls as YsonArray:
            for i in 1..<valueList.count {
                let v = valueList[i]
                switch v {
                case let vList as YsonArray:
                    for vv in vList {
                        ls.add(vv)
                    }
                default:
                    ls.add(v)
                }

            }
            return ls
        default:
            return first
        }
    }

    public func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key: CodingKey {
        let jo = YsonObject()
        push(jo)
        return KeyedEncodingContainer<Key>(ObjectEncoder<Key>(encoder: self, jsonValue: jo))
    }

    public func unkeyedContainer() -> UnkeyedEncodingContainer {
        let ja = YsonArray()
        push(ja)
        return ArrayEncoder(encoder: self, jsonArray: ja)
    }

    public func singleValueContainer() -> SingleValueEncodingContainer {
        ValueEncoder(encoder: self)
    }

    fileprivate func push(_ value: YsonValue, key: String) {
        if let yo = valueList.last as? YsonObject {
            yo.put(key, value)
        }
    }

    fileprivate func push(_ value: YsonValue) {
        if let p = parent {
            if let k = parentKey {
                p.push(value, key: k)
            } else {
                p.push(value)
            }
        } else {
            valueList.append(value)
        }
    }
}

public class ObjectEncoder<K: CodingKey>: KeyedEncodingContainerProtocol {
    public typealias Key = K
    public var codingPath: [CodingKey] = []
    private let encoder: JsonEncoder
    private let jsonValue: YsonObject

    init(encoder: JsonEncoder, jsonValue: YsonObject) {
        self.encoder = encoder
        self.jsonValue = jsonValue
    }

    private func keyName(_ key: Key) -> String {
        encoder.options.namesMap[key.stringValue] ?? key.stringValue
    }


    public func encodeNil(forKey key: Key) throws {
        jsonValue.putNull(keyName(key))
    }

    public func encode(_ value: Bool, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encode(_ value: String, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encode(_ value: Double, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encode(_ value: Float, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encode(_ value: Int, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encode(_ value: Int8, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encode(_ value: Int16, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encode(_ value: Int32, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encode(_ value: Int64, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encode(_ value: UInt, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encode(_ value: UInt8, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encode(_ value: UInt16, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encode(_ value: UInt32, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encode(_ value: UInt64, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encode<T>(_ value: T, forKey key: Key) throws where T: Encodable {
        for en in encoder.options.encoders {
            if en.match(T.self) {
                let yv = en.encodeValue(T.self, value)
                jsonValue.put(keyName(key), yv)
                return
            }
        }
        jsonValue.put(keyName(key),  value.toYson(self.encoder.options))
    }

    public func encodeConditional<T>(_ object: T, forKey key: Key) throws where T: AnyObject, T: Encodable {
        try encode(object, forKey: key)
    }

    public func encodeIfPresent(_ value: Bool?, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encodeIfPresent(_ value: String?, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encodeIfPresent(_ value: Double?, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encodeIfPresent(_ value: Float?, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encodeIfPresent(_ value: Int?, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encodeIfPresent(_ value: Int8?, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encodeIfPresent(_ value: Int16?, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encodeIfPresent(_ value: Int32?, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encodeIfPresent(_ value: Int64?, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encodeIfPresent(_ value: UInt?, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encodeIfPresent(_ value: UInt8?, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encodeIfPresent(_ value: UInt16?, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encodeIfPresent(_ value: UInt32?, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encodeIfPresent(_ value: UInt64?, forKey key: Key) throws {
        jsonValue.put(keyName(key), value)
    }

    public func encodeIfPresent<T>(_ value: T?, forKey key: Key) throws where T: Encodable {
        if value == nil {
            jsonValue.putNull(keyName(key))
        } else {
            for en in encoder.options.encoders {
                if en.match(T.self) {
                    let yv = en.encodeValue(T.self, value!)
                    jsonValue.put(keyName(key), yv)
                    return
                }
            }

            jsonValue.put(keyName(key),  value.toYson(self.encoder.options))
        }
    }

    public func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> where NestedKey: CodingKey {
        let yo = YsonObject()
        jsonValue.put(keyName(key), yo)
        return KeyedEncodingContainer<NestedKey>(ObjectEncoder<NestedKey>(encoder: self.encoder, jsonValue: yo))
    }

    public func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
        let ya = YsonArray()
        jsonValue.put(keyName(key), ya)
        return ArrayEncoder(encoder: self.encoder, jsonArray: ya)
    }

    public func superEncoder() -> Encoder {
        return self.encoder
    }

    //TODO superEncoder(forKey key: Key)
    public func superEncoder(forKey key: Key) -> Encoder {
        JsonEncoder(options: self.encoder.options).apply { e in
            e.parent = self.encoder
            e.parentKey = keyName(key)
        }
    }
}


public class ArrayEncoder: UnkeyedEncodingContainer {

    public var codingPath: [CodingKey] = []
    public var count: Int = 0
    fileprivate let jsonArray: YsonArray
    fileprivate let encoder: JsonEncoder

    init(encoder: JsonEncoder, jsonArray: YsonArray) {
        self.encoder = encoder
        self.jsonArray = jsonArray
    }

    public func encodeNil() throws {
        jsonArray.add(nil)
    }

    public func encode(_ value: Bool) throws {
        jsonArray.add(value)
    }

    public func encode(_ value: String) throws {
        jsonArray.add(value)
    }

    public func encode(_ value: Double) throws {
        jsonArray.add(value)
    }

    public func encode(_ value: Float) throws {
        jsonArray.add(value)
    }

    public func encode(_ value: Int) throws {
        jsonArray.add(value)
    }

    public func encode(_ value: Int8) throws {
        jsonArray.add(value)
    }

    public func encode(_ value: Int16) throws {
        jsonArray.add(value)
    }

    public func encode(_ value: Int32) throws {
        jsonArray.add(value)
    }

    public func encode(_ value: Int64) throws {
        jsonArray.add(value)
    }

    public func encode(_ value: UInt) throws {
        jsonArray.add(value)
    }

    public func encode(_ value: UInt8) throws {
        jsonArray.add(value)
    }

    public func encode(_ value: UInt16) throws {
        jsonArray.add(value)
    }

    public func encode(_ value: UInt32) throws {
        jsonArray.add(value)
    }

    public func encode(_ value: UInt64) throws {
        jsonArray.add(value)
    }

    public func encode<T>(_ value: T) throws where T: Encodable {
        for en in encoder.options.encoders {
            if en.match(T.self) {
                let yv = en.encodeValue(T.self, value)
                jsonArray.add(yv)
                return
            }
        }
        jsonArray.add( value.toYson(self.encoder.options))
    }

    public func encodeConditional<T>(_ object: T) throws where T: AnyObject, T: Encodable {
        try encode(object)
    }

    public func encode<T>(contentsOf sequence: T) throws where T: Sequence, T.Element == Bool {
        let n = jsonArray.count + sequence.underestimatedCount + 8
        jsonArray.data.reserveCapacity(n)
        for value in sequence {
            jsonArray.add(value)
        }
    }

    public func encode<T>(contentsOf sequence: T) throws where T: Sequence, T.Element == String {
        let n = jsonArray.count + sequence.underestimatedCount + 8
        jsonArray.data.reserveCapacity(n)
        for value in sequence {
            jsonArray.add(value)
        }
    }

    public func encode<T>(contentsOf sequence: T) throws where T: Sequence, T.Element == Double {
        let n = jsonArray.count + sequence.underestimatedCount + 8
        jsonArray.data.reserveCapacity(n)
        for value in sequence {
            jsonArray.add(value)
        }
    }

    public func encode<T>(contentsOf sequence: T) throws where T: Sequence, T.Element == Float {
        let n = jsonArray.count + sequence.underestimatedCount + 8
        jsonArray.data.reserveCapacity(n)
        for value in sequence {
            jsonArray.add(value)
        }
    }

    public func encode<T>(contentsOf sequence: T) throws where T: Sequence, T.Element == Int {
        let n = jsonArray.count + sequence.underestimatedCount + 8
        jsonArray.data.reserveCapacity(n)
        for value in sequence {
            jsonArray.add(value)
        }
    }

    public func encode<T>(contentsOf sequence: T) throws where T: Sequence, T.Element == Int8 {
        let n = jsonArray.count + sequence.underestimatedCount + 8
        jsonArray.data.reserveCapacity(n)
        for value in sequence {
            jsonArray.add(value)
        }
    }

    public func encode<T>(contentsOf sequence: T) throws where T: Sequence, T.Element == Int16 {
        let n = jsonArray.count + sequence.underestimatedCount + 8
        jsonArray.data.reserveCapacity(n)
        for value in sequence {
            jsonArray.add(value)
        }
    }

    public func encode<T>(contentsOf sequence: T) throws where T: Sequence, T.Element == Int32 {
        let n = jsonArray.count + sequence.underestimatedCount + 8
        jsonArray.data.reserveCapacity(n)
        for value in sequence {
            jsonArray.add(value)
        }
    }

    public func encode<T>(contentsOf sequence: T) throws where T: Sequence, T.Element == Int64 {
        let n = jsonArray.count + sequence.underestimatedCount + 8
        jsonArray.data.reserveCapacity(n)
        for value in sequence {
            jsonArray.add(value)
        }
    }

    public func encode<T>(contentsOf sequence: T) throws where T: Sequence, T.Element == UInt {
        let n = jsonArray.count + sequence.underestimatedCount + 8
        jsonArray.data.reserveCapacity(n)
        for value in sequence {
            jsonArray.add(value)
        }
    }

    public func encode<T>(contentsOf sequence: T) throws where T: Sequence, T.Element == UInt8 {
        let n = jsonArray.count + sequence.underestimatedCount + 8
        jsonArray.data.reserveCapacity(n)
        for value in sequence {
            jsonArray.add(value)
        }
    }

    public func encode<T>(contentsOf sequence: T) throws where T: Sequence, T.Element == UInt16 {
        let n = jsonArray.count + sequence.underestimatedCount + 8
        jsonArray.data.reserveCapacity(n)
        for value in sequence {
            jsonArray.add(value)
        }
    }

    public func encode<T>(contentsOf sequence: T) throws where T: Sequence, T.Element == UInt32 {
        let n = jsonArray.count + sequence.underestimatedCount + 8
        jsonArray.data.reserveCapacity(n)
        for value in sequence {
            jsonArray.add(value)
        }
    }

    public func encode<T>(contentsOf sequence: T) throws where T: Sequence, T.Element == UInt64 {
        let n = jsonArray.count + sequence.underestimatedCount + 8
        jsonArray.data.reserveCapacity(n)
        for value in sequence {
            jsonArray.add(value)
        }
    }

    public func encode<T>(contentsOf sequence: T) throws where T: Sequence, T.Element: Encodable {
        let n = jsonArray.count + sequence.underestimatedCount + 8
        jsonArray.data.reserveCapacity(n)
        for value in sequence {
            var found = false
            for en in encoder.options.encoders {
                if en.match(T.self) {
                    let yv = en.encodeValue(T.self, value)
                    jsonArray.add(yv)
                    found = true
                }
            }
            if !found {
                jsonArray.add( value.toYson(self.encoder.options))
            }
        }
    }

    public func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> where NestedKey: CodingKey {
        let yo = YsonObject()
        jsonArray.add(yo)
        return KeyedEncodingContainer<NestedKey>(ObjectEncoder<NestedKey>(encoder: self.encoder, jsonValue: yo))
    }

    public func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
        let ya = YsonArray()
        jsonArray.add(ya)
        return ArrayEncoder(encoder: self.encoder, jsonArray: ya)
    }

    public func superEncoder() -> Encoder {
        return self.encoder
    }
}


class ValueEncoder: SingleValueEncodingContainer {
    var codingPath: [CodingKey] = []
    var jsonValue: YsonValue = YsonNull.inst
    private let encoder: JsonEncoder

    init(encoder: JsonEncoder) {
        self.encoder = encoder
    }

    func encodeNil() throws {
        self.encoder.push(YsonNull.inst)
    }

    func encode(_ value: Bool) throws {
        self.encoder.push(YsonBool(value))
    }

    func encode(_ value: String) throws {
        self.encoder.push(YsonString(value))
    }

    func encode(_ value: Double) throws {
        self.encoder.push(YsonNum(value))
    }

    func encode(_ value: Float) throws {
        self.encoder.push(YsonNum(value))
    }

    func encode(_ value: Int) throws {
        self.encoder.push(YsonNum(value))
    }

    func encode(_ value: Int8) throws {
        self.encoder.push(YsonNum(value))
    }

    func encode(_ value: Int16) throws {
        self.encoder.push(YsonNum(value))
    }

    func encode(_ value: Int32) throws {
        self.encoder.push(YsonNum(value))
    }

    func encode(_ value: Int64) throws {
        self.encoder.push(YsonNum(value))
    }

    func encode(_ value: UInt) throws {
        self.encoder.push(YsonNum(value))
    }

    func encode(_ value: UInt8) throws {
        self.encoder.push(YsonNum(value))
    }

    func encode(_ value: UInt16) throws {
        self.encoder.push(YsonNum(value))
    }

    func encode(_ value: UInt32) throws {
        self.encoder.push(YsonNum(value))
    }

    func encode(_ value: UInt64) throws {
        self.encoder.push(YsonNum(value))
    }

    func encode<T>(_ value: T) throws where T: Encodable {
        for en in encoder.options.encoders {
            if en.match(T.self) {
                let yv = en.encodeValue(T.self, value)
                self.encoder.push(yv)
                return
            }
        }
        self.encoder.push( value.toYson(self.encoder.options))
    }
}
